from Tkinter import *
from math import sin, cos, degrees

def ArenaApp(*args, **kwargs):
    """Starts an application containing an Arena frame."""
    app = Tk()
    app.rowconfigure(0, weight=1)
    app.columnconfigure(0, weight=1)
    arena = Arena(app, *args, **kwargs)
    arena.grid(row=0, column=0, sticky=N+S+E+W)
    return arena
    
class Arena(LabelFrame):
    def __init__(self, master, width=600, height=400, title="Arena", 
                 arrow_length=20, arrow_props=dict(fill="black", width=2), 
                 select_props=dict(outline="red", width=3, dash=(20, 10)), 
                 autoupdate=True):
        LabelFrame.__init__(self, master, text=title)
        self.arrow_length = arrow_length
        self.arrow_props = dict(arrow_props)
        self.select_props = dict(select_props)
        self.obstacles = set()
        self.objects = {}       # {obj2d -> data}
        self.tagindex = {}      # {tag -> obj2d}
        self.selected = None
        self.autoupdate = autoupdate
        
        self.canvas = Canvas(self, width=width, height=height, background="white")
        self.xscroll = Scrollbar(self, orient=HORIZONTAL, command=self.canvas.xview)
        self.yscroll = Scrollbar(self, orient=VERTICAL, command=self.canvas.yview)
        self.canvas.configure(xscrollcommand=self.xscroll.set,
                              yscrollcommand=self.yscroll.set)
        self.info = InfoBar(self)
        
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.canvas.grid(row=0, column=0, sticky=N+S+E+W)
        self.yscroll.grid(row=0, column=1, sticky=N+S)
        self.xscroll.grid(row=1, column=0, sticky=E+W)
        self.info.grid(row=2, column=0, columnspan=2, sticky=E+W)
        
        self.canvas.bind("<Button-1>", self.__select)
        self.canvas.bind("<Motion>", self.__update_mousecoords)
        
    def __update_mousecoords(self, event):
        x = self.canvas.canvasx(event.x)
        y = self.canvas.canvasy(event.y)
        self.info.mousecoords.configure(text="x=%d, y=%d" % (x, -y))
        
    def clear(self):
        self.unselect()
        self.canvas.delete(ALL)
        self.info.clear()
        self.obstacles.clear()
        self.objects.clear()
        self.tagindex.clear()
        self.canvas.configure(scrollregion=self.canvas.bbox(ALL))
        if self.autoupdate:
            self.canvas.update_idletasks()
            
    def add(self, obj):
        obj.set_master(self)
        tag = "obj%s" % (id(obj),)
        self.tagindex[tag] = obj
        self.objects[obj] = dict(pos=None,      # previous position
                                 arrow_id=None, # id of arrow canvas item
                                 tag=tag)       # object canvas tag
        self.draw(obj)
        
    def remove(self, obj):
        if obj is self.selected:
            self.unselect()
        objdata = self.objects.pop(obj)
        del self.tagindex[objdata["tag"]]
        self.canvas.delete(objdata["tag"])
        self.canvas.configure(scrollregion=self.canvas.bbox(ALL))
        if self.autoupdate:
            self.canvas.update_idletasks()
        
    def draw(self, obj):
        shape = obj.shape
        if shape is None:
            return
        alpha = obj.heading
        x0, y0 = obj.pos
        x1 = x0 + cos(alpha) * self.arrow_length
        y1 = y0 + sin(alpha) * self.arrow_length
        objdata = self.objects[obj]
        shape.draw(self.canvas, x0, y0, tags=(objdata["tag"],))
        objdata["pos"] = x0, y0
        objdata["arrow_id"] = self.canvas.create_line(x0, -y0, x1, -y1, arrow=LAST, 
                                                      tags=(objdata["tag"],), 
                                                      **self.arrow_props)
        self.canvas.configure(scrollregion=self.canvas.bbox(ALL))
        if self.autoupdate:
            self.canvas.update_idletasks()
        
    def hide(self, obj):
        if obj is self.selected:
            self.unselect()
        objdata = self.objects[obj]
        objdata["hidden"] = True
        self.canvas.delete(objdata["tag"])
        self.canvas.configure(scrollregion=self.canvas.bbox(ALL))
        if self.autoupdate:
            self.canvas.update_idletasks()
        
    def update_position(self, obj):
        if obj.shape is not None:
            objdata = self.objects[obj]
            x0, y0 = objdata["pos"]
            x1, y1 = obj.pos
            objdata["pos"] = (x1, y1)
            self.canvas.move(objdata["tag"], x1 - x0, -(y1 - y0))
            if obj is self.selected:
                self.canvas.move("select", x1 - x0, -(y1 - y0))
            self.canvas.configure(scrollregion=self.canvas.bbox(ALL))
            if self.autoupdate:
                self.canvas.update_idletasks()
            if obj is self.selected:
                self.info.selectioncoords.configure(text="at (%.1f, %.1f)" % (x1, y1))
                
    def update_heading(self, obj):
        if obj.shape is not None:
            objdata = self.objects[obj]
            alpha = obj.heading
            x0, y0 = objdata["pos"]
            x1 = x0 + cos(alpha) * self.arrow_length
            y1 = y0 + sin(alpha) * self.arrow_length
            if obj is self.selected:
                self.canvas.coords("select", *self.canvas.bbox(objdata["tag"]))
                self.info.selectionheading.configure(text="heading %.1f" % (degrees(alpha),))
            self.canvas.coords(objdata["arrow_id"], x0, -y0, x1, -y1)
            if self.autoupdate:
                self.canvas.update_idletasks()
                
    def update_shape(self, obj):
        self.canvas.delete(self.objects[obj]["tag"])
        self.draw(obj)
        if obj is self.selected:
            self.canvas.coords("select", *self.canvas.bbox(self.objects[obj]["tag"]))
            
    # ============================================
    def __select(self, event):
        self.select(self.object_at(self.canvas.canvasx(event.x), 
                                   -self.canvas.canvasy(event.y)))
        
    def __unselect(self, event):
        self.unselect()
        
    def select(self, obj):
        if self.selected is not None and obj is not self.selected:
            self.unselect()
        if obj is not self.selected and obj is not None:
            self.selected = obj
            objdata = self.objects[obj]
            l, t, r, b = self.canvas.bbox(objdata["tag"])
            self.info.selection.configure(text=str(obj))
            self.info.selectioncoords.configure(text="at (%.1f, %.1f)" % obj.pos)
            self.info.selectionheading.configure(text="heading %.1f" % (degrees(obj.heading),))
            self.canvas.create_rectangle(l, t, r, b, fill="", tags=("select",), **self.select_props)
            for sequence, fnc in obj.iter_bindings():
                print "binding", sequence, "to", fnc
                self.bind_all(sequence, fnc)
            self.bind_all("<KeyPress-Escape>", self.__unselect)
            
    def unselect(self):
        if self.selected is None:
            return
        for sequence, _ in self.selected.iter_bindings():
            print "unbinding", sequence
            self.unbind_all(sequence)
        self.unbind_all("<KeyPress-Escape>")
        self.canvas.delete("select")
        self.info.clear_selection()
        self.selected = None
        
    def object_at(self, x, y):
        for item in self.canvas.find_overlapping(x-1, -(y-1), x+1, -(y+1)):
            for tag in self.canvas.gettags(item):
                if tag.startswith("obj"):
                    return self.tagindex[tag]
        return None
        
    def bind(self, obj, sequence, fnc):
        if obj is self.selected:
            self.bind_all(sequence, fnc)
            
    def unbind(self, obj, sequence):
        if obj is self.selected:
            self.unbind_all(sequence)
            
    # ============================================
    def add_obstacle(self, name, shape, x, y):
        if name in self.obstacles:
            raise KeyError("duplicate obstacle name '%s'" % (name,))
        if name.startswith("obj"):
            raise NameError("obstacle names cannot start with 'obj'")
        self.obstacles.add(name)
        shape.draw(self.canvas, x, y, tags=("obstacle", name))
        self.canvas.configure(scrollregion=self.canvas.bbox(ALL))
        if self.autoupdate:
            self.canvas.update_idletasks()
        
    def remove_obstacle(self, name):
        self.obstacles.remove(name)
        self.canvas.delete(name)
        self.canvas.configure(scrollregion=self.canvas.bbox(ALL))
        if self.autoupdate:
            self.canvas.update_idletasks()
        
class InfoBar(LabelFrame):
    def __init__(self, master, title="Info"):
        LabelFrame.__init__(self, master, text=title)
        self.selection = Label(self, text="")
        self.selectioncoords = Label(self, text="")
        self.selectionheading = Label(self, text="")
        self.mousecoords = Label(self, text="")
        
        self.selection.pack(side=LEFT, fill=BOTH)
        self.selectioncoords.pack(side=LEFT, fill=BOTH)
        self.selectionheading.pack(side=LEFT, fill=BOTH)
        self.mousecoords.pack(side=RIGHT, fill=BOTH)
        
    def clear(self):
        self.clear_selection()
        self.mousecoords.configure(text="")
        
    def clear_selection(self):
        self.selection.configure(text="")
        self.selectioncoords.configure(text="")
        self.selectionheading.configure(text="")
        
